package edu.stanford.prpl.impl.app;

import static edu.stanford.prpl.common.PRPLConstants.PRPL_X_REQUEST_DEFAULT_TIMEOUT;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Collections;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.AsyncCallback;
import org.apache.xmlrpc.client.TimingOutCallback;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.apache.xmlrpc.client.TimingOutCallback.TimeoutException;



import edu.stanford.prpl.api.BlobResource;
import edu.stanford.prpl.api.Device;
import edu.stanford.prpl.api.Group;
import edu.stanford.prpl.api.Identity;
import edu.stanford.prpl.api.PRPLAppClient;
import edu.stanford.prpl.api.PRPLObjectManager;
import edu.stanford.prpl.api.QueryResultIterator;
import edu.stanford.prpl.api.Resource;
import edu.stanford.prpl.api.QueryResultIterator.Solution;
import edu.stanford.prpl.common.PRPLConstants;
import edu.stanford.prpl.common.PRPLProperties;
import edu.stanford.prpl.common.util.StringUtils;
import edu.stanford.prpl.common.util.TypeValuePairUtil;
import edu.stanford.prpl.common.util.URIUtils;
import edu.stanford.prpl.common.util.URLUtils;
import edu.stanford.prpl.impl.BlobResourceImpl;
import edu.stanford.prpl.impl.DeviceImpl;
import edu.stanford.prpl.impl.GroupImpl;
import edu.stanford.prpl.impl.IdentityImpl;
import edu.stanford.prpl.impl.ResourceImpl;
import edu.stanford.prpl.impl.app.xmlrpc.PrplXmlRpcHttpTransportFactory;
import edu.stanford.prpl.impl.directory.DirectoryClient;
import edu.stanford.prpl.impl.directory.DirectoryCommon;

class Arrayz {
    static String[] copyOf(Object[] array, int length) {
        String[] retVal = new String[length];
        //System.arraycopy(array, 0, retVal, 0, length);
        
        for (int i = 0; i < length; i++) {
            if (array[i] != null) {
                retVal[i] = array[i].toString();
            }
            else {
                retVal[i] = null;
            }
        }
        return retVal;
    }
}



public class PRPLAppClientImpl implements PRPLAppClient {

	private class AppClientBlobManager extends PRPLAppClientImpl.PRPLBlobServerAppClient {
		// public class AppClientBlobManager implements BlobManager {
		// PRPLObjectManager appClient;

		public AppClientBlobManager(URL hostURL, XmlRpcClient xmlRpcClient) {// PRPLObjectManager appClient) {
			super(hostURL, xmlRpcClient);
			// this.appClient = appClient;
		}

		//@Override
		public InputStream getResourceBlob(String resourceURI) {
			Device blobServer = this.getResourceBlobServerDevice(resourceURI);
			InputStream inputstream = this.getResourceBlob(blobServer.getServerURL(), resourceURI);
			return (inputstream != null) ? inputstream : this.getResourceBlob(serverURL, resourceURI);
		}

		//@Override
		public byte[] getResourceBlob(String resourceURI, int startPosition, int len) {
			Device blobServer = this.getResourceBlobServerDevice(resourceURI);
			byte[] b = this.getResourceBlob(blobServer.getServerURL(), resourceURI, startPosition, len);
			return (b != null) ? b : this.getResourceBlob(serverURL, resourceURI, startPosition, len);
		}

		//@Override
		public Device getResourceBlobServerDevice(String resourceURI) {
			try {
				Map<String, String> resp = (Map<String, String>) xmlRpcClient.execute(
						"prpl.getResourceBlobServerDevice", new Object[] { resourceURI });
				return DirectoryCommon.getDeviceFromPersonalDirectoryEntry((Map<String, String>) resp);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		//@Override
		public boolean setResourceBlob(String resourceURI, InputStream inputStream) {
			Device blobServer = this.getResourceBlobServerDevice(resourceURI);
			boolean success = this.setResourceBlob(blobServer.getServerURL(), resourceURI, inputStream);
			return (success) ? success : this.setResourceBlob(serverURL, resourceURI, inputStream);
		}

		//@Override
		public boolean setResourceBlob(String resourceURI, int startPosition, byte[] b) {
			Device blobServer = this.getResourceBlobServerDevice(resourceURI);
			boolean success = this.setResourceBlob(blobServer.getServerURL(), resourceURI, startPosition, b);
			return (success) ? success : this.setResourceBlob(serverURL, resourceURI, startPosition, b);
		}

	}

	private class AppClientGroupManager implements GroupManager {
		PRPLObjectManager appClient;

		public AppClientGroupManager(PRPLObjectManager appClient) {
			this.appClient = appClient;
		}

		//@Override
		public boolean addIdentityToGroup(String groupURI, String identityURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.addIdentityToGroup", new Object[] { groupURI,
						identityURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		//@Override
		public Group createGroup(String groupName) {
			try {
				String groupUri = (String) xmlRpcClient.execute("prpl.createGroup", new Object[] { groupName });
				return new GroupImpl(groupUri, appClient);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		//@Override
		public Identity[] getAllGroupMembers(String groupURI) {
			try {
				Object[] identityURIs = (Object[]) xmlRpcClient.execute("prpl.getAllGroupMembers",
						new Object[] { groupURI });
				Identity[] identities = new Identity[identityURIs.length];
				for (int i = 0; i < identityURIs.length; i++) {
					identities[i] = new IdentityImpl((String) identityURIs[i], appClient);
				}
				return identities;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		//@Override
		public Group[] getAllGroups() {
			try {
				Object[] groupURIs = (Object[]) xmlRpcClient.execute("prpl.getAllGroups", ARG_EMPTY);
				Group[] groups = new Group[groupURIs.length];
				for (int i = 0; i < groupURIs.length; i++) {
					groups[i] = new GroupImpl((String) groupURIs[i], appClient);
				}
				return groups;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		//@Override
		public Group getGroupByName(String groupName) {
			try {
				// String getGroupByName(String groupName)
				String groupUri = (String) xmlRpcClient.execute("prpl.getGroupByName", new Object[] { groupName });
				if (groupUri != null)
					return new GroupImpl(groupUri, appClient);
				else
					return null;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		//@Override
		public Group getGroupByURI(String groupURI) {
			try {
				String groupUri = (String) xmlRpcClient.execute("prpl.getGroupByURI", new Object[] { groupURI });
				if (groupUri != null)
					return new GroupImpl(groupUri, appClient);
				else
					return null;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		//@Override
		public String getGroupName(String groupURI) {
			try {
				return (String) xmlRpcClient.execute("prpl.getGroupName", new Object[] { groupURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		//@Override
		public Group getPublicGroup() {
			return new GroupImpl(URIUtils.getGroupURI(ownIdentity.getKey(), Group.PUBLIC_GROUP_NAME), appClient);
		}

		//@Override
		public boolean isIdentityGroupMember(String groupURI, String identityURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.isIdentityGroupMember", new Object[] { groupURI,
						identityURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		//@Override
		public boolean removeGroup(String groupURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.removeGroup", new Object[] { groupURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		//@Override
		public boolean removeIdentityFromGroup(String groupURI, String identityURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.removeIdentityFromGroup", new Object[] { groupURI,
						identityURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		// //@Override
		// public boolean setGroupName(String groupURI, String groupName) {
		// try {
		// return (Boolean) xmlRpcClient.execute("prpl.setGroupName", new Object[] { groupURI, groupName });
		// } catch (Exception e) {
		// throw new AppClientException(e);
		// }
		// }

	}

	private class AppClientIdentityManager implements IdentityManager {

		PRPLObjectManager appClient;

		public AppClientIdentityManager(PRPLObjectManager appClient) {
			this.appClient = appClient;
		}

		//@Override
		public Group[] getAllGroupsForIdentity(String identityURI) {
			try {
				Object[] groupURIs = (Object[]) xmlRpcClient.execute("prpl.getAllGroupsForIdentity",
						new Object[] { identityURI });
				Group[] groups = new Group[groupURIs.length];
				for (int i = 0; i < groupURIs.length; i++) {
					groups[i] = new GroupImpl((String) groupURIs[i], appClient);
				}
				return groups;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		//@Override
		public Identity[] getAllIdentities() {
			try {
				Object[] identityURIs = (Object[]) xmlRpcClient.execute("prpl.getAllIdentities", ARG_EMPTY);
				Identity[] identities = new Identity[identityURIs.length];
				for (int i = 0; i < identityURIs.length; i++) {
					identities[i] = new IdentityImpl(identityURIs[i].toString(), appClient);
				}
				return identities;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public Identity getIdentityByKey(String key) {
			try {
				return new IdentityImpl(URIUtils.getIdentityURI(key), appClient);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public Identity getIdentityByURI(String identityURI, boolean create) {
			try {
				String uri = (String) xmlRpcClient.execute("prpl.getIdentityByURI", new Object[] { identityURI,
						create });
				if (uri != null)
					return new IdentityImpl(uri, appClient);
				else
					return null;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public String getIdentityKey(String identityURI) {
			try {
				return (String) xmlRpcClient.execute("prpl.getIdentityKey", new Object[] { identityURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public String getIdentityName(String identityURI) {
			try {
				return (String) xmlRpcClient.execute("prpl.getIdentityName", new Object[] { identityURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean removeIdentity(String identityURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.removeIdentity", new Object[] { identityURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean setIdentityName(String identityURI, String name) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.setIdentityName", new Object[] { identityURI, name });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

	}

	private class AppClientResourceManager implements ResourceManager {

		PRPLObjectManager appClient;

		public AppClientResourceManager(PRPLObjectManager appClient) {
			this.appClient = appClient;
		}

		////@Override
		public boolean addResourceMetaData(String resourceURI, String propertyURI, Object... values) {
			ArrayList<String[]> typeValuePairs = new ArrayList<String[]>();
			for (Object value : values) {
				String[] typeValuePair = TypeValuePairUtil.toTypeValuePair(value);
				if (typeValuePair != null) {
					typeValuePairs.add(typeValuePair);
				}
			}
			try {
				return (Boolean) xmlRpcClient.execute("prpl.addResourceMetaData", new Object[] { resourceURI,
						propertyURI, typeValuePairs });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean addResourceTags(String resourceURI, String... values) {
			// Vector<String> tagVector = new Vector<String>(Arrays.asList(values));
			try {
				return (Boolean) xmlRpcClient
						.execute("prpl.addResourceTags", new Object[] { resourceURI, values });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean addResourceType(String resourceURI, String typeURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.addResourceType",
						new Object[] { resourceURI, typeURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean checkAccessGroupURI(String resourceURI, String groupURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.checkAccessGroupURI", new Object[] { resourceURI,
						groupURI });
			} catch (Exception e) {
				throw new AppClientException();
			}
		}

		////@Override
		public boolean checkAccessIdentityURI(String resourceURI, String identityURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.checkAccessIdentityURI", new Object[] { resourceURI,
						identityURI });
			} catch (Exception e) {
				throw new AppClientException();
			}
		}

		////@Override
		public BlobResource createBlobResource() {
			try {
				String resourceUri = (String) xmlRpcClient.execute("prpl.createBlobResource", ARG_EMPTY);
				return new BlobResourceImpl(resourceUri, appClient);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public BlobResource createBlobResource(String deviceURI) {
			try {
				String resourceUri = (String) xmlRpcClient.execute("prpl.createBlobResource",
						new Object[] { deviceURI });
				return new BlobResourceImpl(resourceUri, appClient);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public Resource createResource() {
			try {
				String resourceUri = (String) xmlRpcClient.execute("prpl.createResource", ARG_EMPTY);
				return new ResourceImpl(resourceUri, appClient);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean destroyResource(String resourceURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.destroyResource", new Object[] { resourceURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public List<BlobResource> getAllBlobResources() {
			try {
				Object[] resourceURIs = (Object[]) xmlRpcClient.execute("prpl.getAllBlobResources", ARG_EMPTY);
				ArrayList<BlobResource> resources = new ArrayList<BlobResource>(resourceURIs.length);
				for (Object resourceURI : resourceURIs) {
					BlobResource resource = new BlobResourceImpl(resourceURI.toString(), appClient);
					resources.add(resource);
				}
				return resources;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public Group[] getAllGroupsSharing(String resourceURI) {
			try {
				Object[] groupURIs = (Object[]) xmlRpcClient.execute("prpl.getAllGroupsSharing",
						new Object[] { resourceURI });
				Group[] groups = new Group[groupURIs.length];
				for (int i = 0; i < groupURIs.length; i++) {
					groups[i] = (new GroupImpl(groupURIs[i].toString(), appClient));
				}
				return groups;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public Identity[] getAllIdentitiesSharing(String resourceURI) {
			try {
				Object[] identityURIs = (Object[]) xmlRpcClient.execute("prpl.getAllIdentitiesSharing",
						new Object[] { resourceURI });
				Identity[] identities = new Identity[identityURIs.length];
				for (int i = 0; i < identityURIs.length; i++) {
					identities[i] = new IdentityImpl(identityURIs[i].toString(), appClient);
				}
				return identities;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public List<Resource> getAllResources() {
			try {
				Object[] resourceURIs = (Object[]) xmlRpcClient.execute("prpl.getAllResources", ARG_EMPTY);
				ArrayList<Resource> resources = new ArrayList<Resource>(resourceURIs.length);
				for (Object resourceURI : resourceURIs) {
					resources.add(new ResourceImpl((String) resourceURI, appClient));
				}
				return resources;

			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public List<Resource> getAllResourcesByPropertyValue(String propertyURI, Object value) {
			try {
				Object[] resourceURIs = (Object[]) xmlRpcClient.execute("prpl.getAllResourcesByPropertyValue",
						new Object[] { propertyURI, TypeValuePairUtil.toTypeValuePair(value) });
				ArrayList<Resource> resources = new ArrayList<Resource>(resourceURIs.length);
				for (Object resourceURI : resourceURIs) {
					resources.add(new ResourceImpl((String) resourceURI, appClient));
				}
				return resources;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public List<Resource> getAllResourcesFrom(String identityURI) {
			try {
				Object[] resourceURIs = (Object[]) xmlRpcClient.execute("prpl.getAllResourcesFrom",
						new Object[] { identityURI });

				ArrayList<Resource> resources = new ArrayList<Resource>(resourceURIs.length);
				for (Object resourceURI : resourceURIs) {
					Resource resource = new ResourceImpl(resourceURI.toString(), appClient);
					resources.add(resource);
				}
				return resources;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public List<Resource> getAllResourcesSharedWithGroup(String groupURI) {
			try {
				Object[] resourceURIs = (Object[]) xmlRpcClient.execute("prpl.getAllResourcesSharedWithGroup",
						new Object[] { groupURI });
				ArrayList<Resource> resources = new ArrayList<Resource>(resourceURIs.length);
				for (Object resourceURI : resourceURIs) {
					Resource resource = new ResourceImpl(resourceURI.toString(), appClient);
					resources.add(resource);
				}
				return resources;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public List<Resource> getAllResourcesSharedWithIdentity(String identityURI) {
			try {
				Object[] resourceURIs = (Object[]) xmlRpcClient.execute("prpl.getAllResourcesSharedWithIdentity",
						new Object[] { identityURI });
				List<Resource> resources = new ArrayList<Resource>(resourceURIs.length);
				for (Object resourceURI : resourceURIs) {
					Resource resource = new ResourceImpl(resourceURI.toString(), appClient);
					resources.add(resource);
				}
				return resources;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public BlobResource getBlobResource(String resourceURI) {
			try {
				return new BlobResourceImpl(resourceURI, appClient);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public Resource getResource(String resourceURI) {
			try {
				return new ResourceImpl(resourceURI, appClient);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public Object[] getResourceMetaData(String resourceURI, String propertyURI) {
			try {
				Object[] pairs = (Object[]) xmlRpcClient.execute("prpl.getResourceMetaData", new Object[] {
						resourceURI, propertyURI });
				Object[] metadata = TypeValuePairUtil.deserializeTypeValuePairs(pairs, appClient);
				return metadata;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public String[] getResourceMetaDataProperties(String resourceURI) {
			try {
				Object[] resp = (Object[]) xmlRpcClient.execute("prpl.getResourceMetaDataProperties",
						new Object[] { resourceURI });

				
				return Arrayz.copyOf(resp, resp.length);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public long getResourceMetaLastModifiedDate(String resourceURI) {
			try {
				Double time = (Double) xmlRpcClient.execute("prpl.getResourceMetaLastModifiedDate",
						new Object[] { resourceURI });
				return (time != null) ? time.longValue() : 0;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public String getResourceName(String resourceURI) {
			try {
				return (String) xmlRpcClient.execute("prpl.getResourceName", new Object[] { resourceURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public Identity getResourceOwner(String resourceURI) {
			try {
				String identityUri = (String) xmlRpcClient.execute("prpl.getResourceOwner",
						new Object[] { resourceURI });
				return new IdentityImpl(identityUri, appClient);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public String[] getResourceTags(String resourceURI) {
			try {
				Object[] resp = (Object[]) xmlRpcClient.execute("prpl.getResourceTags",
						new Object[] { resourceURI });
				return Arrayz.copyOf(resp, resp.length);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public String[] getResourceTypes(String resourceURI) {
			try {
				Object[] resp = (Object[]) xmlRpcClient.execute("prpl.getResourceTypes",
						new Object[] { resourceURI });
				return Arrayz.copyOf(resp, resp.length);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean isBlobResource(String resourceURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.isBlobResource", new Object[] { resourceURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean isOwn(String resourceURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.isOwn", new Object[] { resourceURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean removeResourceMetaData(String resourceURI, String propertyURI, Object... values) {
			ArrayList<String[]> typeValuePairs = new ArrayList<String[]>();
			for (Object value : values) {
				String[] typeValuePair = TypeValuePairUtil.toTypeValuePair(value);
				if (typeValuePair != null) {
					typeValuePairs.add(typeValuePair);
				}
			}
			try {
				return (Boolean) xmlRpcClient.execute("prpl.removeResourceMetaData", new Object[] { resourceURI,
						propertyURI, typeValuePairs });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean removeResourceTags(String resourceURI, String... values) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.removeResourceTags", new Object[] { resourceURI,
						values });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean removeResourceType(String resourceURI, String typeURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.removeResourceType", new Object[] { resourceURI,
						typeURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public QueryResultIterator runQuery(String query) {
			try {
				Object[] solutions = (Object[]) xmlRpcClient.execute("prpl.runQuery", new Object[] { query });

				List<Map<String, ?>> solutionMaps = new ArrayList<Map<String, ?>>();
				for (Object solution : solutions) {
					Map<String, Object> solutionMap = (Map<String, Object>) solution;

					for (String key : solutionMap.keySet()) {
						Object[] value = (Object[]) solutionMap.get(key);
						String[] typeValuePair = Arrayz.copyOf(value, value.length);
						solutionMap.put(key, TypeValuePairUtil.fromTypeValuePair(typeValuePair, appClient));
					}
					solutionMaps.add(solutionMap);
				}
				return new AppClientQueryResult(solutionMaps);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public QueryResultIterator runQuery(String query, Solution initialBinding) {
			try {
				// Vector<Hashtable<String, ?>> runQuery(String query, Hashtable initBindings)
				Map map = new HashMap();
				Iterator<String> iterator = initialBinding.varNames();
				while (iterator.hasNext()) {
					String s = iterator.next();
					// TODO: convert and preserve object types (Resources, Identities, Groups)
					map.put(s, initialBinding.get(s));
				}

				Object[] solutions = (Object[]) xmlRpcClient.execute("prpl.runQuery", new Object[] { query, map });
				List<Map<String, ?>> solutionMaps = new ArrayList<Map<String, ?>>();
				for (Object solution : solutions) {
					solutionMaps.add((Map<String, ?>) solution);
				}
				return new AppClientQueryResult(solutionMaps);
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean setResourceMetaData(String resourceURI, String propertyURI, Object... values) {
			ArrayList<String[]> typeValuePairs = new ArrayList<String[]>();
			for (Object value : values) {
				String[] typeValuePair = TypeValuePairUtil.toTypeValuePair(value);
				if (typeValuePair != null) {
					typeValuePairs.add(typeValuePair);
				}
			}
			try {
				return (Boolean) xmlRpcClient.execute("prpl.setResourceMetaData", new Object[] { resourceURI,
						propertyURI, typeValuePairs });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean setResourceMetaLastModifiedDate(String resourceURI, long date) {
			// XMLRPC truncates milliseconds from java.util.Dates during serialization
			try {
				return (Boolean) xmlRpcClient.execute("prpl.setResourceMetaLastModifiedDate", new Object[] {
						resourceURI, (double) date });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean setResourceName(String resourceURI, String name) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.setResourceName", new Object[] { resourceURI, name });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean shareResourceWithGroups(String resourceURI, String... groupURIs) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.shareResourceWithGroups", new Object[] { resourceURI,
						groupURIs });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean shareResourceWithIdentities(String resourceURI, String... identityURIs) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.shareResourceWithIdentities", new Object[] {
						resourceURI, identityURIs });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean unshareResourceWithGroups(String resourceURI, String... groupURIs) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.unshareResourceWithGroups", new Object[] {
						resourceURI, groupURIs });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean unshareResourceWithIdentities(String resourceURI, String... identityURIs) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.unshareResourceWithIdentities", new Object[] {
						resourceURI, identityURIs });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}
	}

	public static class BatchRequestImpl implements PRPLAppClient.BatchRequest {

		List<Map<String, ?>> requests;

		public BatchRequestImpl() {
			this.requests = new LinkedList<Map<String, ?>>();
		}

		public boolean addRequest(String methodName, Object[] params) {
			Map req = new HashMap();
			req.put(METHOD_NAME, methodName);
			req.put(PARAMS, params);
			return this.requests.add(req);
		}

		public List getBatch() {
			return this.requests;
		}

	}

	public static class PRPLBlobServerAppClient implements BlobManager {

		private static Log logger = LogFactory.getLog(PRPLBlobServerAppClient.class);

		XmlRpcClient xmlRpcClient;
		URL serverURL;

		public PRPLBlobServerAppClient(URL hostURL, XmlRpcClient xmlRpcClient) {
			this.xmlRpcClient = xmlRpcClient;
			this.serverURL = hostURL;
		}

		public PRPLBlobServerAppClient(URL hostURL, String userKey, String userPassword) {
			XmlRpcClientConfigImpl xmlRpcClientConfig = new XmlRpcClientConfigImpl();
			xmlRpcClientConfig.setServerURL(hostURL);
			xmlRpcClientConfig.setBasicUserName(userKey);
			xmlRpcClientConfig.setBasicPassword(userPassword);
			xmlRpcClientConfig.setEnabledForExtensions(true);
			xmlRpcClientConfig.setContentLengthOptional(true);
			xmlRpcClientConfig.setConnectionTimeout(PRPL_X_REQUEST_DEFAULT_TIMEOUT);
			xmlRpcClientConfig.setReplyTimeout(PRPL_X_REQUEST_DEFAULT_TIMEOUT);
			this.serverURL = hostURL;
			this.xmlRpcClient = new XmlRpcClient();
			xmlRpcClient.setConfig(xmlRpcClientConfig);
			// Replace with our XmlRpcHttpTransportFacotory
			xmlRpcClient.setTransportFactory(new PrplXmlRpcHttpTransportFactory(xmlRpcClient));
		}

		////@Override
		public InputStream getResourceBlob(String resourceURI) {
			InputStream inputstream = this.getResourceBlob(this.serverURL, resourceURI);
			return inputstream;
		}

		////@Override
		public byte[] getResourceBlob(String resourceURI, int startPosition, int len) {
			byte[] b = this.getResourceBlob(this.serverURL, resourceURI, startPosition, len);
			return b;
		}

		////@Override
		public InputStream getResourceBlob(URL blobServerURL, String resourceURI) {
			try {
				HttpURLConnection conn = URLUtils.getHttpURLConnection(blobServerURL);
				conn.addRequestProperty(PRPLConstants.PRPL_X_REQUEST, PRPLConstants.PRPL_X_REQUEST_BLOB_READ);
				conn.addRequestProperty(PRPLConstants.PRPL_X_REQUEST_PARAMS, resourceURI);
				conn.connect();
				InputStream inputstream = conn.getInputStream();
				return inputstream;
			} catch (IOException e) {
				logger.error(e);
			}
			return null;
		}

		////@Override
		public byte[] getResourceBlob(URL blobServerURL, String resourceURI, int startPosition, int len) {
			try {
				HttpURLConnection conn = URLUtils.getHttpURLConnection(blobServerURL);
				conn.addRequestProperty(PRPLConstants.PRPL_X_REQUEST, PRPLConstants.PRPL_X_REQUEST_BLOB_READ);
				conn.addRequestProperty(PRPLConstants.PRPL_X_REQUEST_PARAMS, resourceURI + "," + startPosition
						+ "," + len);
				conn.connect();
				InputStream inputstream = conn.getInputStream();

				byte[] b = new byte[1024];
				ByteBuffer bf = ByteBuffer.allocate(len);
				int read;
				while ((read = inputstream.read(b)) > 0 && bf.hasRemaining()) {
					bf.put(b, 0, read);
				}
				return bf.array();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			return null;
		}

		////@Override
		public String getResourceBlobContentHash(String resourceURI) {
			try {
				return (String) xmlRpcClient.execute("prpl.getResourceBlobContentHash",
						new Object[] { resourceURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public long getResourceBlobLastModifiedDate(String resourceURI) {
			try {
				Double time = (Double) xmlRpcClient.execute("prpl.getResourceBlobLastModifieedDate",
						new Object[] { resourceURI });
				return (time != null) ? time.longValue() : 0;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public String getResourceBlobMimeType(String resourceURI) {
			try {
				return (String) xmlRpcClient.execute("prpl.getResourceBlobMimeType", new Object[] { resourceURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public Device getResourceBlobServerDevice(String resourceURI) {
			throw new AppClientException("Unsupported request to BlobServer");
			// try {
			// Map<String, String> resp = (Map<String, String>) xmlRpcClient.execute(
			// "prpl.getResourceBlobServerDevice", new Object[] { resourceURI });
			// return DirectoryCommon.getDeviceFromPersonalDirectoryEntry((Map<String, String>) resp);
			// } catch (Exception e) {
			// throw new AppClientException(e);
			// }
		}

		////@Override
		public long getResourceBlobSize(String resourceURI) {
			try {
				Double size = (Double) xmlRpcClient.execute("prpl.getResourceBlobSize",
						new Object[] { resourceURI });
				return (size != null) ? size.longValue() : 0;
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public String getResourceBlobSourceURI(String resourceURI) {
			try {
				return (String) xmlRpcClient
						.execute("prpl.getResourceBlobSourceURI", new Object[] { resourceURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean removeResourceBlob(String resourceURI) {
			// TODO Auto-generated method stub
			return false;
		}

		////@Override
		public boolean setResourceBlob(String resourceURI, InputStream inputStream) {
			return this.setResourceBlob(this.serverURL, resourceURI, inputStream);
		}

		////@Override
		public boolean setResourceBlob(String resourceURI, int startPosition, byte[] b) {
			return this.setResourceBlob(this.serverURL, resourceURI, startPosition, b);
		}

		////@Override
		public boolean setResourceBlob(URL blobServerURL, String resourceURI, InputStream inputStream) {
			try {
				HttpURLConnection conn = URLUtils.getHttpURLConnection(blobServerURL);
				conn.addRequestProperty(PRPLConstants.PRPL_X_REQUEST, PRPLConstants.PRPL_X_REQUEST_BLOB_WRITE);
				conn.addRequestProperty(PRPLConstants.PRPL_X_REQUEST_PARAMS, resourceURI);
				conn.connect();
				OutputStream outputstream = conn.getOutputStream();
				byte[] b = new byte[1024];
				int read;
				while ((read = inputStream.read(b)) > 0) {
					outputstream.write(b, 0, read);
				}
				outputstream.close();
				return true;
			} catch (IOException e) {
				logger.error(e);
			}
			return false;
		}

		////@Override
		public boolean setResourceBlob(URL blobServerURL, String resourceURI, int startPosition, byte[] b) {
			try {
				HttpURLConnection conn = URLUtils.getHttpURLConnection(blobServerURL);
				conn.addRequestProperty(PRPLConstants.PRPL_X_REQUEST, PRPLConstants.PRPL_X_REQUEST_BLOB_WRITE);
				conn.addRequestProperty(PRPLConstants.PRPL_X_REQUEST_PARAMS, resourceURI + "," + startPosition
						+ "," + b.length);
				conn.connect();
				OutputStream outputstream = conn.getOutputStream();
				outputstream.write(b);
				outputstream.close();
				return true;
			} catch (IOException e) {
				logger.error(e);
			}
			return false;
		}

		////@Override
		public boolean setResourceBlobContentHash(String resourceURI, String contentHash) {
			try {
				// void setResourceContentHash(String resourceURI, String contentHash)
				return (Boolean) xmlRpcClient.execute("prpl.setResourceBlobContentHash", new Object[] {
						resourceURI, contentHash });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean setResourceBlobLastModifiedDate(String resourceURI, long date) {
			// XMLRPC truncates milliseconds from java.util.Dates during serialization
			try {
				return (Boolean) xmlRpcClient.execute("prpl.setResourceBlobLastModifiedDate", new Object[] {
						resourceURI, (double) date });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean setResourceBlobMimeType(String resourceURI, String mimeType) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.setResourceBlobMimeType", new Object[] { resourceURI,
						mimeType });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean setResourceBlobSize(String resourceURI, long size) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.setResourceBlobSize", new Object[] { resourceURI,
						(double) size });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean setResourceBlobSourceURI(String resourceURI, String sourceURI) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.setResourceBlobSourceURI", new Object[] { resourceURI,
						sourceURI });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean removeResourceBlobWatcher(String resourceURI, String watchMode, String callbackURL) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.removeResourceBlobWatcher", new Object[] {
						resourceURI, watchMode, callbackURL });
				// resourceURI, watchMode, callbackURL.toURI().toASCIIString() });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}

		////@Override
		public boolean setResourceBlobWatcher(String resourceURI, String watchMode, String callbackURL) {
			try {
				return (Boolean) xmlRpcClient.execute("prpl.setResourceBlobWatcher", new Object[] { resourceURI,
						watchMode, callbackURL });
				// watchMode, callbackURL.toURI().toASCIIString() });
			} catch (Exception e) {
				throw new AppClientException(e);
			}
		}
	}

	private static Log logger = LogFactory.getLog(PRPLAppClientImpl.class);

	private static final Object[] ARG_EMPTY = new Object[0];

	public static PRPLAppClient newInstance() {

		String userKey = StringUtils.trimEvenNull(PRPLProperties.getProperty("prpl.user.key"));
		String userPW = StringUtils.trimEvenNull(PRPLProperties.getProperty("prpl.user.password"));

		Device device = DeviceImpl.getCurrentDevice();

		try {
			if (device != null && device.isPCM()) {
				String apiUrl = PRPLProperties.getProperty("prpl.device.server.api.url");
				String apiPort = PRPLProperties.getProperty("prpl.device.server.api.port", Integer
						.toString(PRPLConstants.DEFAULT_PCM_PORT));
				String apiName = PRPLProperties.getProperty("prpl.device.server.api.name");
				String url = MessageFormat.format(apiUrl, "127.0.0.1", apiPort, apiName);

				URL hostURL = new URL(url);
				PRPLAppClient prplAppClient = new PRPLAppClientImpl(hostURL, userKey, userPW);
				return prplAppClient;

			} else {
				DirectoryClient dirClient = DirectoryClient.getGlobalDirectoryServiceClient();
				Map<String, String> entry = dirClient.getEntry(URIUtils.getIdentityURI(userKey));
				if (entry != null) {
					URL hostURL = new URL(entry.get(DirectoryCommon.IDENTITY_SERVER_URL));
					PRPLAppClient prplAppClient = new PRPLAppClientImpl(hostURL, userKey, userPW);
					return prplAppClient;
				}
			}
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;

	}

	public static PRPLAppClient newInstance(String identityKey) {

		String userKey = PRPLProperties.getProperty("prpl.user.key");

		try {
			DirectoryClient dirClient = DirectoryClient.getGlobalDirectoryServiceClient();
			Map<String, String> entry = dirClient.getEntry(URIUtils.getIdentityURI(identityKey));
			if (entry != null) {
				String host = entry.get(DirectoryCommon.IDENTITY_SERVER_URL);
				PRPLAppClient prplAppClient = new PRPLAppClientImpl(new URL(host), userKey);
				return prplAppClient;
			}
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;

	}

	private XmlRpcClient xmlRpcClient;

	private URL serverURL;

	private Identity ownIdentity;
	// private boolean batchMode;
	// private BatchRequest batchRequest;

	private ResourceManager resourceManager;

	private BlobManager blobManager;

	private GroupManager groupManager;

	private IdentityManager identityManager;

	public PRPLAppClientImpl(URL hostURL, String userKey) {
		// Instantiate without password
		this(hostURL, userKey, "");
	}

	public PRPLAppClientImpl(URL hostURL, String userKey, String userPassword) {
		this.serverURL = hostURL;
		String identityURI = URIUtils.getIdentityURI(userKey);

		XmlRpcClientConfigImpl xmlRpcClientConfig = new XmlRpcClientConfigImpl();
		xmlRpcClientConfig.setServerURL(hostURL);
		// xmlRpcClientConfig.setBasicUserName(identityURI);
		xmlRpcClientConfig.setBasicUserName(userKey);
		xmlRpcClientConfig.setBasicPassword(userPassword);
		xmlRpcClientConfig.setEnabledForExtensions(true);
		xmlRpcClientConfig.setContentLengthOptional(true);
		xmlRpcClientConfig.setConnectionTimeout(PRPL_X_REQUEST_DEFAULT_TIMEOUT);
		xmlRpcClientConfig.setReplyTimeout(PRPL_X_REQUEST_DEFAULT_TIMEOUT);

		this.xmlRpcClient = new XmlRpcClient();
		xmlRpcClient.setConfig(xmlRpcClientConfig);
		// Replace with our XmlRpcHttpTransportFacotory
		xmlRpcClient.setTransportFactory(new PrplXmlRpcHttpTransportFactory(xmlRpcClient));

		this.resourceManager = new AppClientResourceManager(this);
		this.blobManager = new AppClientBlobManager(hostURL, this.xmlRpcClient);
		// this.blobManager = new AppClientBlobManager(this);
		this.groupManager = new AppClientGroupManager(this);
		this.identityManager = new AppClientIdentityManager(this);

		this.ownIdentity = new IdentityImpl(identityURI, this);
		// this.batchMode = false;
	}

	////@Override
	public boolean clearIndexOf(String identityURI) {
		try {
			return (Boolean) xmlRpcClient.execute("prpl.clearIndexOf", new Object[] { identityURI });
		} catch (Exception e) {
			throw new AppClientException(e);
		}
	}

	////@Override
	public List<?> executeBatch(BatchRequest batch) {
		try {
			Object[] resp = (Object[]) xmlRpcClient.execute("prpl.multicall", batch.getBatch());
			return Arrays.asList(resp);
		} catch (XmlRpcException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

	////@Override
	public Device[] getAllBlobServerDevices() {
		try {
			List<Device> devices = new LinkedList<Device>();
			Object[] resp = (Object[]) xmlRpcClient.execute("prpl.getAllBlobServerDevices", new Object[] {});
			for (Object entry : resp) {
				if (entry instanceof Map) {
					Device device = DirectoryCommon
							.getDeviceFromPersonalDirectoryEntry((Map<String, String>) entry);
					devices.add(device);
				}
			}
			return devices.toArray(new Device[] {});
		} catch (Exception e) {
			throw new AppClientException(e);
		}
	}

	////@Override
	public Device[] getAllDevices() {
		try {
			List<Device> devices = new LinkedList<Device>();
			Object[] resp = (Object[]) xmlRpcClient.execute("prpl.getAllDevices", new Object[] {});
			for (Object entry : resp) {
				if (entry instanceof Map) {
					Device device = DirectoryCommon
							.getDeviceFromPersonalDirectoryEntry((Map<String, String>) entry);
					devices.add(device);
				}
			}
			return devices.toArray(new Device[] {});
		} catch (Exception e) {
			throw new AppClientException(e);
		}
	}

	////@Override
	public BlobManager getBlobManager() {
		return this.blobManager;
	}

	////@Override
	public Device getDevice(String deviceURI) {
		try {
			Map<String, String> resp = (Map<String, String>) xmlRpcClient.execute("prpl.getDevice",
					new Object[] { deviceURI });
			return DirectoryCommon.getDeviceFromPersonalDirectoryEntry((Map<String, String>) resp);
		} catch (XmlRpcException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

	////@Override
	public GroupManager getGroupManager() {
		return this.groupManager;
	}

	////@Override
	public IdentityManager getIdentityManager() {
		return this.identityManager;
	}

	public long getIndexLastModifiedDate() {
		try {
			Double time = (Double) xmlRpcClient.execute("prpl.getIndexLastModifiedDate", new Object[] {});
			return (time != null) ? time.longValue() : 0;
		} catch (Exception e) {
			throw new AppClientException(e);
		}
	}

	////@Override
	public Identity getOwnIdentity() {
		return this.ownIdentity;
	}

	////@Override
	public ResourceManager getResourceManager() {
		return this.resourceManager;
	}

	////@Override
	public URL getServerURL() {
		return this.serverURL;
	}

	public boolean pullIndexFrom(String identityURI) {
		try {
			return (Boolean) xmlRpcClient.execute("prpl.pullIndexFrom", new Object[] { identityURI });
		} catch (Exception e) {
			throw new AppClientException(e);
		}
	}

	// ////@Override
	// public void beginBatch() {
	// this.batchRequest = new BatchRequestImpl();
	// this.batchMode = true;
	// }

	public boolean pushIndexTo(String identityURI) {
		try {
			return (Boolean) xmlRpcClient.execute("prpl.pushIndexTo", new Object[] { identityURI });
		} catch (Exception e) {
			throw new AppClientException(e);
		}
	}

	public boolean syncIndexWith(String identityURI) {
		try {
			return (Boolean) xmlRpcClient.execute("prpl.syncIndexWith", new Object[] { identityURI });
		} catch (Exception e) {
			throw new AppClientException(e);
		}
	}

	// public boolean registerPersistentQuery(String resourceURI, AsyncCallback callback) {
	// // TimingOutCallback callback = new TimingOutCallback(10 * 1000);
	// xmlRpcClient.executeAsync("prpl.registerPersistentQuery", new Object[] {resourceURI}, callback);
	// try {
	// return callback.waitForResponse();
	// } catch (TimeoutException e) {
	// System.out.println("No response from server.");
	// } catch (Exception e) {
	// System.out.println("Server returned an error message.");
	// }
	// }

}
